# 优化总结

什么是前端性能优化?

通常来说我们说的前端性能优化是指:从用户开始访问我们的网站到整个页面完整地展现出来的过程中,通过各种优化策略和优化方法,让页面加载的更快,让用户的操作响应更及时,给用户更好的使用体验。

前端性能优化是我们每个前端打工人都必须面对的问题,那怎么进行性能优化呢?下面我就给大家分享我所了解的一些性能优化知识,有什么疑问请大家提出来,谢谢~

# 性能优化指标与测量工具

工欲善其事必先利其器,在进行性能优化之前,我们应该准备好自己的性能优化测量工具,并且要有优化目标,即某项或者多项指标需要优化到什么程度。

# 常用的性能优化指标

  • Speed Index(lighthouse,速度指数)
  • TTFB(Network,第一个请求响应时间)
  • 页面加载时间
  • 首次渲染
  • 交互动作的反馈时间
  • 帧率FPS(动画 ctrl+shift+p)
  • 异步请求完成时间

# 性能测量工具

  • Chrome DevTools

    • 开发调试、性能评测
    • Audit(Lighthouse)
    • Throttling 调整网络吞吐
    • Performance 性能分析
    • Network 网络加载分析
  • Lighthouse

    • 网站整体质量评估
    • 还可以提出优化建议
  • WebPageTest

    • 测试多地点(球各地的用户访问你的网站的性能情况)
    • 全面性能报告(first view,repeat view,waterfall chart 等等)
    • WebPageTest 还可以进行本地安装,让你的应用在还没上线的时候就可以测试。
    • 更多请看 官网 (opens new window)

# 常用的性能测量API

计算一些关键的性能指标

window.addEventListener('load', (event) => {
    // Time to Interactive
    let timing = performance.getEntriesByType('navigation')[0];
	// let timing = performance.timing
    console.log(timing.domInteractive);
    console.log(timing.fetchStart);
    let diff = timing.domInteractive - timing.fetchStart;
    console.log("TTI: " + diff);
})
1
2
3
4
5
6
7
8
9

以上代码只测量了"首次可交互时间",其它常用的性能指标还有:

DNS 解析耗时: domainLookupEnd - domainLookupStart
TCP 连接耗时: connectEnd - connectStart
SSL 安全连接耗时: connectEnd - secureConnectionStart
网络请求耗时 (TTFB): responseStart - requestStart
数据传输耗时: responseEnd - responseStart
DOM 解析耗时: domInteractive - responseEnd
资源加载耗时: loadEventStart - domContentLoadedEventEnd
First Byte时间: responseStart - domainLookupStart
白屏时间: responseEnd - fetchStart
首次可交互时间: domInteractive - fetchStart
DOM Ready 时间: domContentLoadEventEnd - fetchStart
页面完全加载时间: loadEventStart - fetchStart
http 头部大小: transferSize - encodedBodySize
重定向次数:performance.navigation.redirectCount
重定向耗时: redirectEnd - redirectStart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

观察长任务(long tasks)

// 通过 PerformanceObserver 得到所有的 long task 对象
const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        console.log(entry)
    }
})
// 监听 long task
observer.observe({entryTypes: ['longtask']})
1
2
3
4
5
6
7
8

监听页面可见性的状态

let vEvent = 'visibilitychange';
if (document.webkitHidden != undefined) {
    // webkit prefix detected
    vEvent = 'webkitvisibilitychange';
}

function visibilityChanged() {
    if (document.hidden || document.webkitHidden) {
        console.log("Web page is hidden.")
    } else {
        console.log("Web page is visible.")
    }
}

document.addEventListener(vEvent, visibilityChanged, false);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

通过监听页面可见性的状态可以判断用户是否在看当前页面,可以做一些处理,比如视频暂停,保存状态等等

判断用户网络状态

var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
var type = connection.effectiveType;

function updateConnectionStatus() {
  console.log("Connection type changed from " + type + " to " + connection.effectiveType);
  type = connection.effectiveType;
}

connection.addEventListener('change', updateConnectionStatus);
1
2
3
4
5
6
7
8
9

通过判断用户网络状态,可以做一些针对性的加载,比如用户网络状态好,就可以加载一些更加高清的图片,反之加载一些质量更差的图片等等。

# 传输加载优化

  • 减少 HTTP 请求
  • 启用 Gzip 压缩
  • 启用 Keep Alive 长连接(默认)
  • HTTP 资源缓存
  • Service workers
    • 加速重复访问
    • 离线支持
    • 需要注意延长了首屏加载时间,但是页面总加载时间减少
  • 启用 HTTP2
  • 考虑用SSR技术
  • 异步无阻塞加载JS(defer,async)

# 资源优化

  • 资源压缩与合并
  • 图片格式优化,选择合适的图片格式
  • 图片加载优化(懒加载,渐进式加载,响应式图片,雪碧图)
  • 字体优化
    • 字体未下载完成时,浏览器隐藏或自动降级,导致字体闪烁
    • font-display
    • @font-face 字符集拆分
    • ajax + base64

图片格式选择

JPEG

非常适合: 颜色丰富的照片,彩色图大焦点图、通栏 banner 图,结构不规则的图形

不适合:线条图形和文字、图标图形,因为它的压缩算法不太适合这些类型的图形,并且不支持透明度

压缩工具:jpegtran

PNG

非常适合: 纯色、透明、线条绘图、图标;边缘清晰、有大块相同颜色区域;颜色数较少单需要半透明

不适合:由于是无损存储,彩色图像体积太大,所以不太适合颜色丰富,彩色大图

压缩工具: node-pngquant-native

GIF

非常适合:动画,图标

不适合:每个像素只有 8 比特,不适合存储彩色图片

压缩工具:Gifsicle

Webp

非常适合:适用于图形和半透明图像

不适合: 最多处理 256 色,不适合彩色图片

真的需要图片吗

Web font 代替图片

使用 Data URI 代替图片

采用 Image spriting(雪碧图)

# 渲染优化

  • 避免回流与重绘
  • 避免布局抖动(FastDom防止布局抖动利器)
  • 将动画和经常变化的元素提到单独的图层

# 代码优化

# JS 优化

  • Code splitting 代码拆分,按需加载
  • Tree shaking 代码减重
  • Scope Hoisting 模块合并
  • 去除 console
  • 避免长任务
  • 避免超过 1kb 的行间脚本
  • 大量事件绑定使用事件委托
  • 适时使用防抖和节流
  • 避免内存泄漏
  • 使用 rAF 和 rIC 进行时间调度(react时间调度原理)
  • 配合 V8 有效优化代码
    • 源码 -> 抽象语法树 -> 字节码Bytecode -> 机器码
    • 编译过程会进行优化(脚本流、字节码缓存、懒解析等)
    • 运行时可能会发生反优化
  • 对象优化可以做哪些
    • 以相同顺序初始化对象成员,避免隐藏类的调整
    • 实例化后避免添加新属性
    • 尽量使用 Array 代替 array-like 对象
    • 避免读取超过数组的长度
    • 避免元素类型转换

# HTML 优化

  • 精简 HTML 代码
  • 减少 iframes 使用,延迟加载 iframe,父文档加载完毕后再给 iframe 的 src 赋值
  • 压缩空白符
  • 避免节点深层级嵌套
  • 避免 table 布局
  • 删除注释
  • CSS&Javascript尽量外链
  • 删除元素默认属性

# CSS 优化

提升 CSS 渲染性能

  • 谨慎使用 expensive 昂贵的属性
    • 如 :nth-child 伪类; position: fixed 定位
  • 尽量减少样式层级数量
    • 如 div ul li span i {color: true;}
  • 尽量避免使用占用过多 CPU 和内存的属性
    • 如 text-indent: -9999px
  • 尽量避免使用耗电量大的属性
    • 如 CSS3:3D transforms、CSS3 transitions、Opacity
  • 尽量避免使用 CSS 表达式
    • background-color: expression((new Date()).getHours() % 2 ? "#FFF" : "#000")
  • 尽量避免使用通配选择器,有选择地使用选择器
    • body > a {font-weight: blod;}
  • 尽量避免类正则的属性选择器
    • *=,|=,^=,$=
  • 使用 contain 属性,告诉浏览器某些方面可以这样优化,哪些不能优化
  • 使用 font-display 属性,帮助我们把文字更早显示在页面上,还可以减少文字闪动问题

提升 CSS 文件加载性能

  • 内联首屏关键CSS(Critical CSS)
  • 使用外链的 CSS
  • 尽量避免使用 @import
  • 异步加载CSS

精简 CSS 代码

  • 使用后缩写语句
  • 删除不必要的零
  • 删除不必要的单位,如px
  • 删除过多的分号
  • 删除空格的注释
  • 尽量减少样式表的大小

合理使用 Web Fonts

  • 将字体部署在 CDN 上
  • 将字体以 base64 形式保存在 CSS 中并通过 localStorage 进行缓存
  • Google 字体库因为某些不可抗原因,应该使用国内托管服务

CSS 动画优化

  • 尽量避免同时动画
  • 延迟动画初始化
  • 结合 SVG

# 构建优化

加快构建速度(打包速度)

使用 speed-measure-webpack-plugin 插件可以测量各个插件和loader所花费的时间,量化打包速度,判断优化效果

  1. 缩小文件的搜索范围(配置include/exclude resolve.modules resolve.mainFields alias noParse extensions)
    1. 通过 exclude、include 配置来确保转译尽可能少的文件
    2. 优化 resolve.modules 配置
    3. 优化 resolve.mainFields 配置
    4. alias
    5. noParse
    6. extensions
  2. 在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中
  3. 使用 happypack 开启多进程打包
  4. 除了使用 Happypack 外,我们也可以使用 thread-loader 开启多进程打包 loader
  5. 使用 HardSourceWebpackPlugin 为模块提供中间缓存,第二次构建可大量节约时间
  6. 使用 IgnorePlugin 忽略第三方包指定目录,例如 moment 的本地语言包
  7. 使用 webpack-parallel-uglify-plugin 开启 JS 多进程压缩

减少打包文件体积

引入 webpack-bundle-analyzer 分析打包后的文件,判断哪些包还可以拆分和优化

  1. 使用 externals 配置,然后将 JS 文件、CSS 文件和存储在 CDN
  2. 使用 DllPlugin(动态链接库)将 bundles 拆分,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间
  3. 使用 optimization.splitChunks 配置抽离公共代码
  4. 使用 IgnorePlugin 忽略第三方包指定目录,例如 moment 的本地语言包(重复)
  5. 使用 url-loader 或 image-webpack-loader 对图片进行转化或者压缩处理
  6. 优化 SourceMap,开发环境推荐: cheap-module-eval-source-map,生产环境推荐: cheap-module-source-map
  7. 按需加载,项目中的路由懒加载
  8. webpack自身的优化:
    1. tree-shaking,在生产环境下,会自动移除没有使用到的代码
    2. scope hosting 作用域提升,变量提升,可以减少一些变量声明
    3. babel 配置的优化,配置 @babel/plugin-transform-runtime,重复使用 Babel 注入的帮助程序,以节省代码大小的插件

# 更多流行优化技术

  • 移动端使用 SVG 图标
  • 使用 Flexbox 优化布局
  • 优化资源加载顺序,给资源设置优先级
    • Preload:提前加载较晚出现,但对当前页面非常重要的资源
    • Prefetch:提前加载后继路由需要的资源,优先级低
    • dns-prefetch:对链接进行 DNS 预解析
    • prerender:标识下一个导航可能需要的资源
  • 预渲染页面(react-snap,vue也可以使用)
  • Windowing、虚拟列表等提高列表性能
  • 使用骨架组件减少布局移动和页面抖动

不同的应用场景可能会采用不同的优化方案和优化技术,对于一个应用,不能所有的优化都一股脑想用上去,因为优化也会带来一定的成本,比如配置麻烦,需要遵守一些特定的规则等等。

# 数据库优化

  1. 查询语句无论是使用哪种判断条件:等于、小于、大于,WHERE 左侧的条件查询字段不要使用函数或者表达式
  2. 使用 EXPLAIN 命令优化你的 SELECT 查询,对于复杂、效率低的 SQL 语句,我们通常是使用 explain sql 来分析这条 SQL 语句,这样方便我们分析,进行优化
  3. 当你的 SELECT 查询语句只需要使用一条记录时,要使用 LIMIT 1
  4. 不要直接使用 SELECT *,而应该使用需要查询的表字段,因为使用 EXPLAIN 进行分析时,SELECT * 使用的是全表扫描,也就是 type = all
  5. 为没一张表设置一个 ID 属性
  6. 避免再 WHERE 字句中对字段进行 NULL 判断
  7. 避免再 WHERE 中使用 != 或 <> 操作符
  8. 使用 BETWEEN AND 替代 IN
  9. 为搜索字段创建索引
  10. 选择正确的存储引擎,innoDB、MyISAM、MEMORY 等
  11. 使用 LINK %abc% 不会走索引,而使用 LINK abc% 会走索引
  12. 对于美剧类型的字段(即有固定罗列值得字段),建议使用 EMUM 而不是 VARCHAR,如性别、星期、类型、类别等
  13. 拆分打得 DELETE 或 INSERT 语句
  14. 选择合适得字段类型,选择标准是:尽可能小、尽可能定长、尽可能使用整数
  15. 字段设计尽可能使用 NOT NULL
  16. 进行水平切割或者垂直分割

水平分割:通过建立结构相同得几张表分别存储数据 垂直分割:将经常一起使用得字段放在一个单独的表中,分割后的表记录之间是一一对应的关系

# java 代码优化

  1. 尽量指定类、方法的 final 修饰符
  2. 尽量重用对象
  3. 尽可能使用局部变量
  4. 及时关闭流
  5. 尽量减少对变量的重复计算
  6. 尽量采用懒加载的策略,即在需要的时候才创建
  7. 慎用异常
  8. 不要在循环中使用 try...catch...,应该把其放在最外层
  9. 如果能估计到待添加的内容长度,为底层以数组方式实现的集合、工具类指定初始长度
  10. 当复制大量数据时,使用System.arraycopy()命令
  11. 乘法和除法使用移位操作
  12. 循环内不要不断创建对象引用
  13. 基于效率和类型检查的考虑,应该尽可能使用array,无法确定数组大小时才使用 ArrayList
  14. 尽量使用 HashMap、ArrayList、StringBuilder,除非线程安全需要,否则不推荐使用 Hashtable、Vector、StringBuffer,后三者由于使用同步机制而导致了性能开销
  15. 不要将数组声明为 public static final
  16. 尽量在合适的场合使用单例
  17. 尽量避免随意使用静态变量
  18. 及时清除不再需要的会话
  19. 避免内存泄漏
  20. 字符串变量和字符串常量 equals 的时候将字符串常量写在前面,避免空指针异常

更多请看:java 代码优化 (opens new window)